/*
 * Graphic.cpp
 *
 *  Created on: 2012-9-15
 *      Author: yangentao@gmail.com
 */

#include <windows.h>
#include <windowsx.h>

#include "../util/util.h"
#include "Graphic.h"


class GraphicImpl: public Graphic {
	HDC hdc;
	bool hdcOwned;
	HPEN pen;
	HPEN penOld;
	HBRUSH brush;
	HBRUSH brushOld;
	HFONT font;
	HFONT fontOld;
	HBITMAP bitmap;
	HBITMAP bitmapOld;

	void BrushColor(Color back);
	void SetFont(Font &font_);

	// Private so GraphicImpl objects can not be copied
	GraphicImpl(const GraphicImpl &);
	GraphicImpl &operator=(const GraphicImpl &);
public:
	GraphicImpl();
	virtual ~GraphicImpl();

	void Init(WindowID wid);
	void Init(GraphicID sid, WindowID wid);

	void Release();
	bool isEnable();
	void PenColour(Color fore);
	int LogPixelsY();
	int DeviceHeightFont(int points);
	void MoveTo(int x_, int y_);
	void LineTo(int x_, int y_);
	void Polygon(Point *pts, int npts, Color fore, Color back);
	void RectangleDraw(Rect rc, Color fore, Color back);
	void FillRectangle(Rect rc, Color back);
	void RoundedRectangle(Rect rc, Color fore, Color back);
	void AlphaRectangle(Rect rc, int cornerSize, Color fill, int alphaFill, Color outline, int alphaOutline, int flags);
	void Ellipse(Rect rc, Color fore, Color back);

	void DrawTextCommon(Rect rc, Font &font_, int ybase, const char *s, int len, UINT fuOptions);
	void DrawTextNoClip(Rect rc, Font &font_, int ybase, const char *s, int len, Color fore, Color back);
	void DrawTextClipped(Rect rc, Font &font_, int ybase, const char *s, int len, Color fore, Color back);
	void DrawTextTransparent(Rect rc, Font &font_, int ybase, const char *s, int len, Color fore);
	void MeasureWidths(Font &font_, const char *s, int len, int *positions);
	int WidthText(Font &font_, const char *s, int len);
	int WidthChar(Font &font_, char ch);
	int Ascent(Font &font_);
	int Descent(Font &font_);
	int InternalLeading(Font &font_);
	int ExternalLeading(Font &font_);
	int Height(Font &font_);
	int AverageCharWidth(Font &font_);

	void SetClip(Rect rc);
};

GraphicImpl::GraphicImpl()
		: hdc(0), hdcOwned(false), pen(0), penOld(0), brush(0), brushOld(0), font(0), fontOld(0), bitmap(0), bitmapOld(0) {
}

GraphicImpl::~GraphicImpl() {
	Release();
}

void GraphicImpl::Release() {
	if (penOld) {
		SelectPen(hdc, penOld);
		penOld = 0;
	}
	if (pen) {
		DeletePen(pen);
		pen = 0;
	}
	if (brushOld) {
		SelectBrush(hdc, brushOld);
		brushOld = 0;
	}
	if (brush) {
		::DeleteBrush(brush);
		brush = 0;
	}
	if (fontOld) {
		// Fonts are not deleted as they are owned by a Font object
		SelectFont(hdc, fontOld);
		fontOld = 0;
	}
	font = 0;
	if (bitmapOld) {
		SelectBitmap(hdc, bitmapOld);
		bitmapOld = 0;
	}
	if (bitmap) {
		::DeleteBitmap(bitmap);
		bitmap = 0;
	}

	if (hdcOwned) {
		::DeleteDC(reinterpret_cast<HDC>(hdc));
		hdc = 0;
		hdcOwned = false;
	}
}

bool GraphicImpl::isEnable() {
	return hdc != 0;
}

void GraphicImpl::Init(WindowID) {
	Release();
	hdc = ::CreateCompatibleDC(NULL); //TODO why compatible DC, not GetDC(window)?
	hdcOwned = true;
	::SetTextAlign(reinterpret_cast<HDC>(hdc), TA_BASELINE);
}

void GraphicImpl::Init(GraphicID sid, WindowID) {
	Release();
	hdc = reinterpret_cast<HDC>(sid);
	::SetTextAlign(reinterpret_cast<HDC>(hdc), TA_BASELINE);
}

void GraphicImpl::PenColour(Color fore) {
	if (pen) {
		DeletePen(pen);
	}
	pen = ::CreatePen(0, 1, fore);
	penOld = SelectPen(hdc, pen);
}

void GraphicImpl::BrushColor(Color back) {
	if (brush) {
		DeleteBrush(brush);
	}
	// Only ever want pure, non-dithered brushes
	Color colourNearest = ::GetNearestColor(hdc, back);
	brush = ::CreateSolidBrush(colourNearest);
	brushOld = SelectBrush(hdc, brush);
}

void GraphicImpl::SetFont(Font &font_) {
	if (font_.GetID() != font) {
		fontOld = SelectFont(hdc, font_);
		font = font_;
	}
}

int GraphicImpl::LogPixelsY() {
	return ::GetDeviceCaps(hdc, LOGPIXELSY);
}

int GraphicImpl::DeviceHeightFont(int points) {
	return ::MulDiv(points, LogPixelsY(), 72);
}

void GraphicImpl::MoveTo(int x_, int y_) {
	::MoveToEx(hdc, x_, y_, 0);
}

void GraphicImpl::LineTo(int x_, int y_) {
	::LineTo(hdc, x_, y_);
}

void GraphicImpl::Polygon(Point *pts, int npts, Color fore, Color back) {
	PenColour(fore); //TODO delete it
	BrushColor(back);
	::Polygon(hdc, reinterpret_cast<POINT *>(pts), npts);
}

void GraphicImpl::RectangleDraw(Rect rc, Color fore, Color back) {
	PenColour(fore);
	BrushColor(back);
	::Rectangle(hdc, rc.left, rc.top, rc.right, rc.bottom);
}

void GraphicImpl::FillRectangle(Rect rc, Color back) {
	// Using ExtTextOut rather than a FillRect ensures that no dithering occurs.
	// There is no need to allocate a brush either.
	::SetBkColor(hdc, back);
	::ExtTextOut(hdc, rc.left, rc.top, ETO_OPAQUE, &rc, TEXT(""), 0, NULL);
}

void GraphicImpl::RoundedRectangle(Rect rc, Color fore, Color back) {
	PenColour(fore);
	BrushColor(back);
	//TODO why -1 ?
	::RoundRect(hdc, rc.left + 1, rc.top, rc.right - 1, rc.bottom, 8, 8);
}

// Plot a point into a DWORD buffer symetrically to all 4 qudrants
static void AllFour(DWORD *pixels, int width, int height, int x, int y, DWORD val) {
	pixels[y * width + x] = val;
	pixels[y * width + width - 1 - x] = val;
	pixels[(height - 1 - y) * width + x] = val;
	pixels[(height - 1 - y) * width + width - 1 - x] = val;
}

#ifndef AC_SRC_OVER
#define AC_SRC_OVER                 0x00
#endif
#ifndef AC_SRC_ALPHA
#define AC_SRC_ALPHA		0x01
#endif

static DWORD dwordFromBGRA(byte b, byte g, byte r, byte a) {
	union {
		byte pixVal[4];
		DWORD val;
	} converter;
	converter.pixVal[0] = b;
	converter.pixVal[1] = g;
	converter.pixVal[2] = r;
	converter.pixVal[3] = a;
	return converter.val;
}

void GraphicImpl::AlphaRectangle(Rect rc, int cornerSize, Color fill, int alphaFill, Color outline, int alphaOutline, int /* flags*/) {
	if (rc.Width() > 0) {
		HDC hMemDC = ::CreateCompatibleDC(hdc);
		int width = rc.Width();
		int height = rc.Height();
		// Ensure not distorted too much by corners when small
		cornerSize = Min(cornerSize, (Min(width, height) / 2) - 2);
		BITMAPINFO bpih = { sizeof(BITMAPINFOHEADER), width, height, 1, 32, BI_RGB, 0, 0, 0, 0, 0 };
		void *image = 0;
		HBITMAP hbmMem = CreateDIBSection(hMemDC, &bpih, DIB_RGB_COLORS, &image, NULL, 0);

		HBITMAP hbmOld = SelectBitmap(hMemDC, hbmMem);

		DWORD valEmpty = dwordFromBGRA(0, 0, 0, 0);
		DWORD valFill = dwordFromBGRA(static_cast<byte>(GetBValue(fill.AsLong()) * alphaFill / 255), static_cast<byte>(GetGValue(fill.AsLong()) * alphaFill / 255), static_cast<byte>(GetRValue(fill.AsLong()) * alphaFill / 255), static_cast<byte>(alphaFill));
		DWORD valOutline = dwordFromBGRA(static_cast<byte>(GetBValue(outline.AsLong()) * alphaOutline / 255), static_cast<byte>(GetGValue(outline.AsLong()) * alphaOutline / 255), static_cast<byte>(GetRValue(outline.AsLong()) * alphaOutline / 255),
				static_cast<byte>(alphaOutline));
		DWORD *pixels = reinterpret_cast<DWORD *>(image);
		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				if ((x == 0) || (x == width - 1) || (y == 0) || (y == height - 1)) {
					pixels[y * width + x] = valOutline;
				} else {
					pixels[y * width + x] = valFill;
				}
			}
		}
		for (int c = 0; c < cornerSize; c++) {
			for (int x = 0; x < c + 1; x++) {
				AllFour(pixels, width, height, x, c - x, valEmpty);
			}
		}
		for (int x = 1; x < cornerSize; x++) {
			AllFour(pixels, width, height, x, cornerSize - x, valOutline);
		}

		BLENDFUNCTION merge = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };

		AlphaBlend(hdc, rc.left, rc.top, width, height, hMemDC, 0, 0, width, height, merge);

		SelectBitmap(hMemDC, hbmOld);
		DeleteBitmap(hbmMem);
		DeleteDC(hMemDC);
	} else {
		BrushColor(outline);
		FrameRect(hdc, &rc, brush);
	}
}

void GraphicImpl::Ellipse(Rect rc, Color fore, Color back) {
	PenColour(fore);
	BrushColor(back);
	::Ellipse(hdc, rc.left, rc.top, rc.right, rc.bottom);
}

void GraphicImpl::DrawTextCommon(Rect rc, Font &font_, int ybase, const char *s, int len, UINT fuOptions) {
	SetFont(font_);
	SIZE sz = { 0, 0 };
	int pos = 0;
	int x = rc.left;

	// Text drawing may fail if the text is too big.
	// If it does fail, slice up into segments and draw each segment.
	const int maxSegmentLength = 0x200;

	// Use Unicode calls
	const TextWide tbuf(s, len);
	if (!::ExtTextOutW(hdc, x, ybase, fuOptions, &rc, tbuf.buf, tbuf.len, NULL)) {
		while (tbuf.len > pos) {
			int seglen = Min(maxSegmentLength, tbuf.len - pos);
			if (!::ExtTextOutW(hdc, x, ybase, fuOptions, &rc, tbuf.buf + pos, seglen, NULL)) {
				assert(false);
				return;
			}
			::GetTextExtentPoint32W(hdc, tbuf.buf + pos, seglen, &sz);
			x += sz.cx;
			pos += seglen;
		}
	}
}

void GraphicImpl::DrawTextNoClip(Rect rc, Font &font_, int ybase, const char *s, int len, Color fore, Color back) {
	::SetTextColor(hdc, fore.AsLong());
	::SetBkColor(hdc, back.AsLong());
	DrawTextCommon(rc, font_, ybase, s, len, ETO_OPAQUE);
}

void GraphicImpl::DrawTextClipped(Rect rc, Font &font_, int ybase, const char *s, int len, Color fore, Color back) {
	::SetTextColor(hdc, fore.AsLong());
	::SetBkColor(hdc, back.AsLong());
	DrawTextCommon(rc, font_, ybase, s, len, ETO_OPAQUE | ETO_CLIPPED);
}

void GraphicImpl::DrawTextTransparent(Rect rc, Font &font_, int ybase, const char *s, int len, Color fore) {
	// Avoid drawing spaces in transparent mode
	//TODO why ?
	for (int i = 0; i < len; i++) {
		if (s[i] != ' ') {
			::SetTextColor(hdc, fore.AsLong());
			::SetBkMode(hdc, TRANSPARENT);
			DrawTextCommon(rc, font_, ybase, s, len, 0);
			::SetBkMode(hdc, OPAQUE);
			return;
		}
	}
}

int GraphicImpl::WidthText(Font &font_, const char *s, int len) {
	SetFont(font_);
	SIZE sz = { 0, 0 };
	const TextWide tbuf(s, len);
	::GetTextExtentPoint32W(hdc, tbuf.buf, tbuf.len, &sz);
	return sz.cx;
}

void GraphicImpl::MeasureWidths(Font &font_, const char *s, int len, int *positions) {
	SetFont(font_);
	SIZE sz = { 0, 0 };
	int fit = 0;
	const TextWide tbuf(s, len);
	Vector<int> poses(tbuf.len);
	fit = tbuf.len;
	if (!::GetTextExtentExPointW(hdc, tbuf.buf, tbuf.len, tbuf.len, &fit, poses.getBuffer(), &sz)) {
		// Likely to have failed because on Windows 9x where function not available
		// So measure the character widths by measuring each initial substring
		// Turns a linear operation into a qudratic but seems fast enough on test files
		for (int widthSS = 0; widthSS < tbuf.len; widthSS++) {
			::GetTextExtentPoint32W(hdc, tbuf.buf, widthSS + 1, &sz);
			poses[widthSS] = sz.cx;
		}
	}
	// Map the widths given for UTF-16 characters back onto the UTF-8 input string
	int ui = 0;
	const unsigned char *us = reinterpret_cast<const unsigned char *>(s);
	int i = 0;
	while (ui < fit) {
		unsigned char uch = us[i];
		unsigned int lenChar = 1;
		if (uch >= (0x80 + 0x40 + 0x20 + 0x10)) {
			lenChar = 4;
			ui++;
		} else if (uch >= (0x80 + 0x40 + 0x20)) {
			lenChar = 3;
		} else if (uch >= (0x80)) {
			lenChar = 2;
		}
		for (unsigned int bytePos = 0; (bytePos < lenChar) && (i < len); bytePos++) {
			positions[i++] = poses[ui];
		}
		ui++;
	}
	int lastPos = 0;
	if (i > 0)
		lastPos = positions[i - 1];
	while (i < len) {
		positions[i++] = lastPos;
	}
}

int GraphicImpl::WidthChar(Font &font_, char ch) {
	SetFont(font_);
	SIZE sz;
	::GetTextExtentPoint32A(hdc, &ch, 1, &sz);
	return sz.cx;
}

int GraphicImpl::Ascent(Font &font_) {
	SetFont(font_);
	TEXTMETRIC tm;
	::GetTextMetrics(hdc, &tm);
	return tm.tmAscent;
}

int GraphicImpl::Descent(Font &font_) {
	SetFont(font_);
	TEXTMETRIC tm;
	::GetTextMetrics(hdc, &tm);
	return tm.tmDescent;
}

int GraphicImpl::InternalLeading(Font &font_) {
	SetFont(font_);
	TEXTMETRIC tm;
	::GetTextMetrics(hdc, &tm);
	return tm.tmInternalLeading;
}

int GraphicImpl::ExternalLeading(Font &font_) {
	SetFont(font_);
	TEXTMETRIC tm;
	::GetTextMetrics(hdc, &tm);
	return tm.tmExternalLeading;
}

int GraphicImpl::Height(Font &font_) {
	SetFont(font_);
	TEXTMETRIC tm;
	::GetTextMetrics(hdc, &tm);
	return tm.tmHeight;
}

int GraphicImpl::AverageCharWidth(Font &font_) {
	SetFont(font_);
	TEXTMETRIC tm;
	::GetTextMetrics(hdc, &tm);
	return tm.tmAveCharWidth;
}



void GraphicImpl::SetClip(Rect rc) {
	::IntersectClipRect(hdc, rc.left, rc.top, rc.right, rc.bottom);
}


Graphic* Graphic::instance() {

	return new GraphicImpl();
}
